"""A class to encapsulate running a single game."""

__author__ = "Gabriel Taubman (gtaubman@mac.com)"

import copy
import random
import unittest
import test_players

from board import Board
from constants import TILES_IN_HAND
from move import Move
from example_player import ExamplePlayer
from tile_bag import TileBag
from word_list import WordList

class BadGameSetupError(Exception):
  def __init__(self, reason):
    self.reason = reason

  def __str__(self):
    return self.reason

class GameOutcome:
  # Game end reasons:
  CLEAN_GAME = 0
  PLAYER_CHEATED = 1

  def __init__(self, reason):
    # A dictionary from Player to integer score.
    self.scores = {}

    # A list of anyone who cheated in this game.
    self.cheaters = []

    self.reason = reason

class Game:
  """The Game class encapsulates the running of a single game.

  A game continues running until there are no players left.  This means that if
  all but one player resigns, the one player left can continue to go until there
  are no tiles left.

  When exchanging tiles, new tiles are drawn from the bag before inserting the
  exchanged tiles.  This makes sure that you don't get back any of the tiles you
  put in, and if you attempt to exchange all of your tiles with an empty bag you
  end up with no tiles and you must resign.

  The board state is checksummed before and after each player's move to ensure
  no cheating has taken place.
  """
  def __init__(self, wordlist = None):
    self.board = Board(wordlist)
    self.bag = TileBag()
    self.players = []
    self.active_players = []
    self.current_player = 0
    self.tiles_for_player = {}
    self.score_for_player = {}
    self.players_that_have_passed = set()

  def AddPlayer(self, player):
    self.players.append(player)
    self.active_players.append(player)
    self.tiles_for_player[player] = []
    self.score_for_player[player] = 0

  def CreateGameOutcome(self, cheaters = None):
    if cheaters == None:
      outcome = GameOutcome(GameOutcome.CLEAN_GAME)
    else:
      outcome = GameOutcome(GameOutcome.PLAYER_CHEATED)

    outcome.scores = self.score_for_player
    outcome.cheaters = cheaters
    return outcome

  def RunGame(self):
    """Returns a list of players sorted by decreasing score."""
    if not len(self.active_players):
      raise BadGameSetupError("Must have at least one player.")
    else:
      # Pick a random order of players.
      random.shuffle(self.active_players)

    # Give each player their tiles.
    for i in range(TILES_IN_HAND):
      for player in self.active_players:
        self.tiles_for_player[player].append(self.bag.Draw())

    while len(self.active_players):
      # Advance player.
      self.current_player = (self.current_player + 1) % len(self.active_players)
      player = self.active_players[self.current_player]

      # Get an encoded hash of the board's state.  This is used to ensure that
      # the player hasn't changed the internal state of the board during their
      # term.
      pre_move_board = self.board.BoardHash()
      pre_move_tiles = copy.deepcopy(self.tiles_for_player[player])
      move = player.GetMove(self.board, self.tiles_for_player[player])
      post_move_board = self.board.BoardHash()
      post_move_tiles = self.tiles_for_player[player]
      if (pre_move_board != post_move_board or
          pre_move_tiles != post_move_tiles):
        return self.CreateGameOutcome(cheaters = [player])

      if (move.type != Move.PASS and move.type != move.NONE and
          player in self.players_that_have_passed):
        # Remove any player who isn't passing from our list of passing players.
        self.players_that_have_passed.remove(player)

      if move.type == Move.PLAY_TILES:
        # First off, make sure that the player actually played tiles that they
        # were given.
        for position, letter in move.IterTiles():
          if not letter in pre_move_tiles:
            return self.CreateGameOutcome(cheaters = [player])

        self.score_for_player[player] += self.board.ScoreForMove(move)

        # Tally the score and replace any used tiles.
        for position, letter in move.Tiles():
          self.board[position].SetLetter(letter)
          self.tiles_for_player[player].remove(letter)
          new_tile = self.bag.Draw()
          if new_tile != None:
            self.tiles_for_player[player].append(new_tile)
      elif move.type == Move.EXCHANGE_TILES:
        for position, letter in move.Tiles():
          new_letter = self.bag.Draw()
          if new_letter!= None:
            self.tiles_for_player[player].append(new_letter)
          self.tiles_for_player[player].remove(letter)
      elif move.type == Move.DONE:
        self.active_players.remove(player)
      elif move.type == Move.NONE or move.type == Move.PASS:
        # The player either didn't do anything, or explicitly passed.  If all
        # the active players have passed then end the game.
        self.players_that_have_passed.add(player)
        if len(self.players_that_have_passed) == len(self.active_players):
          self.active_players = []

    print str(self.board)
    return self.CreateGameOutcome()

class GameTest(unittest.TestCase):
  def TestCheater(self, player_type):
    g = Game()
    g.AddPlayer(player_type())
    g.AddPlayer(player_type())

    result = g.RunGame()

    # Make sure the game-end reason is marked as a cheating player.
    self.assert_(result.reason == GameOutcome.PLAYER_CHEATED)

    # There should be one cheater, and it should be whoever went first.
    self.assert_(len(result.cheaters) == 1)
    self.assert_(result.cheaters[0] == g.active_players[1])

  def TestGameSucceeds(self, players):
    g = Game()

    for player in players:
      g.AddPlayer(player)

    result = g.RunGame()

    # Make sure the game ended successfully.
    self.assert_(result.reason == GameOutcome.CLEAN_GAME,
                 "Reason was %d." % result.reason)

  def testPlayingPiecesWeDontHave(self):
    # Make sure that if a player plays pieces that they don't actually have that
    # we catch that and mark them as losing the game, and everyone else as a
    # draw.
    self.TestCheater(test_players.PlaysTilesHeDoesntHavePlayer)

  def testAlteringWordMultipliers(self):
    # Make sure that if a player tries to change a word multiplier we notice.
    self.TestCheater(test_players.AltersBoardWordMultiplierPlayer)

  def testAlteringLetterMultipliers(self):
    # Make sure that if a player tries to change a letter multiplier we notice.
    self.TestCheater(test_players.AltersBoardLetterMultiplierPlayer)

  def testAlteringBoardLetters(self):
    # Make sure that if a player adds tiles directly to the board we notice.
    self.TestCheater(test_players.AltersBoardLettersPlayer)

  def testChangingPlayersTiles(self):
    # Make sure that if a player attempts to change his own tiles we notice.
    self.TestCheater(test_players.SmarterPlaysTilesHeDoesntHavePlayer)

  def testPassingPlayerOne(self):
    self.TestGameSucceeds([test_players.EmptyMovePlayer(), ExamplePlayer()])

  def testPassingPlayerTwo(self):
    self.TestGameSucceeds([test_players.EmptyMovePlayer(),
                           test_players.EmptyMovePlayer(),
                           ExamplePlayer()])

if __name__ == "__main__":
  unittest.main()
